﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using Aeon.Emulator.Decoding;

namespace Aeon.Emulator.DebugSupport
{
    /// <summary>
    /// Generates x86 disassembly from an arbitrary source.
    /// </summary>
    [CLSCompliant(false)]
    public sealed class Disassembler : INotifyPropertyChanged, IDataErrorInfo
    {
        #region Private Fields
        /// <summary>
        /// IMachineCodeSource with machine code.
        /// </summary>
        private readonly IMachineCodeSource source;
        /// <summary>
        /// The initial code segment.
        /// </summary>
        private ushort cs;
        /// <summary>
        /// The decoding base address calculated from the initial code segment.
        /// </summary>
        private uint? csBase;
        /// <summary>
        /// The initial code offset.
        /// </summary>
        private uint eip;
        /// <summary>
        /// The maximum number of instructions to decode.
        /// </summary>
        private int maximum;
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the Disassembler class.
        /// </summary>
        /// <param name="memory">IMachineCodeSource containing instructions to decode.</param>
        public Disassembler(IMachineCodeSource source)
        {
            if(source == null)
                throw new ArgumentNullException("source");

            this.source = source;
        }
        #endregion

        #region Public Events
        /// <summary>
        /// Occurs when a property value has changed.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion

        #region Public Properties
        /// <summary>
        /// Gets or sets the starting code segment for instruction decoding.
        /// </summary>
        public ushort StartSegment
        {
            get { return this.cs; }
            set
            {
                if(this.cs != value)
                {
                    this.cs = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("StartSegment"));
                }

                this.SegmentBaseAddress = this.source.GetBaseAddress(this.cs);
                OnPropertyChanged(new PropertyChangedEventArgs("Instructions"));
            }
        }
        /// <summary>
        /// Gets or sets the starting code offset for instruction decoding.
        /// </summary>
        public uint StartOffset
        {
            get { return this.eip; }
            set
            {
                if(this.eip != value)
                {
                    this.eip = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("StartOffset"));
                    OnPropertyChanged(new PropertyChangedEventArgs("Instructions"));
                }
            }
        }
        /// <summary>
        /// Gets the base address calculated from the segment.
        /// </summary>
        public uint? SegmentBaseAddress
        {
            get { return this.csBase; }
            private set
            {
                if(this.csBase != value)
                {
                    this.csBase = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("SegmentBaseAddress"));
                }
            }
        }
        /// <summary>
        /// Gets or sets the maximum number of instructions to decode.
        /// </summary>
        public int MaximumInstructions
        {
            get { return this.maximum; }
            set
            {
                if(this.maximum != value)
                {
                    this.maximum = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("MaximumInstructions"));
                    OnPropertyChanged(new PropertyChangedEventArgs("Instructions"));
                }
            }
        }
        /// <summary>
        /// Gets the disassembled instructions.
        /// </summary>
        public IEnumerable<Instruction> Instructions
        {
            get { return Disassemble(); }
        }
        /// <summary>
        /// Gets a descriptive error message for invalid property values.
        /// </summary>
        string IDataErrorInfo.Error
        {
            get
            {
                if(((IDataErrorInfo)this)["StartSegment"] != string.Empty)
                    return ((IDataErrorInfo)this)["StartSegment"];
                if(((IDataErrorInfo)this)["MaximumInstructions"] != string.Empty)
                    return ((IDataErrorInfo)this)["MaximumInstructions"];

                return string.Empty;
            }
        }
        #endregion

        #region Public Indexers
        /// <summary>
        /// Gets a descriptive message error for a property.
        /// </summary>
        /// <param name="columnName">Name of the property.</param>
        /// <returns>Descriptive error message for the property.</returns>
        string IDataErrorInfo.this[string columnName]
        {
            get
            {
                if(columnName == "StartSegment")
                {
                    if(this.SegmentBaseAddress == null)
                        return "Invalid code segment.";
                }
                else if(columnName == "MaximumInstructions")
                {
                    if(this.MaximumInstructions <= 0)
                        return "Maximum instructions must be greater than zero.";
                }

                return string.Empty;
            }
        }
        #endregion

        #region Private Static Methods
        /// <summary>
        /// Returns the prefix value of a prefix opcode.
        /// </summary>
        /// <param name="opcode">Prefix opcode.</param>
        /// <returns>Prefix value of the opcode.</returns>
        private static PrefixState GetPrefix(OpcodeInfo opcode)
        {
            switch(opcode.Opcode)
            {
            case 0x2E:
                return PrefixState.CS;

            case 0x36:
                return PrefixState.SS;

            case 0x3E:
                return PrefixState.DS;

            case 0x26:
                return PrefixState.ES;

            case 0x64:
                return PrefixState.FS;

            case 0x65:
                return PrefixState.GS;

            case 0x66:
                return PrefixState.OperandSize;

            case 0x67:
                return PrefixState.AddressSize;

            case 0xF2:
                return PrefixState.Repne;

            case 0xF3:
                return PrefixState.Repe;
            }

            return PrefixState.None;
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Fills the buffer with disassembled instructions.
        /// </summary>
        private IEnumerable<Instruction> Disassemble()
        {
            if(this.csBase == null)
                yield break;

            byte[] instBuffer = new byte[16];
            uint baseAddress = (uint)this.csBase;
            uint offset = this.eip;
            var prefixes = PrefixState.None;

            for(int i = 0; i < this.maximum; i++)
            {
                this.source.ReadInstruction(instBuffer, baseAddress + offset);
                var opcode = InstructionSet.Decode(instBuffer);
                var inst = new Instruction(opcode, instBuffer, this.cs, offset, false) { Prefixes = prefixes };
                if(opcode != null && opcode.IsPrefix)
                {
                    prefixes |= GetPrefix(opcode);
                    i--;
                }
                else
                {
                    prefixes = PrefixState.None;
                    yield return inst;
                }

                offset += (uint)inst.UnprefixedLength;
            }
        }
        /// <summary>
        /// Raises the PropertyChanged event.
        /// </summary>
        /// <param name="e">Information about the event.</param>
        private void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            var handler = this.PropertyChanged;
            if(handler != null)
                handler(this, e);
        }
        #endregion
    }
}
